Skip to main content

Browser-apis

Data Attributes and the dataset API​

Definition​

  • Custom data attributes start with data- and are accessible through the dataset property.

Reading Data Attributes​

<div
id="user"
data-user-id="123"
data-role="admin"
data-is-active="true"
>
John Doe
</div>


const user = document.querySelector('#user')

// Read data attributes (camelCase!)
console.log(user.dataset.userId) // "123"
console.log(user.dataset.role) // "admin"

Adding or Updating Data Attributes​

user.dataset.lastLogin = '2024-01-15'

// Creates:
// data-last-login="2024-01-15"

Deleting Data Attributes​

delete user.dataset.role

Event Delegation in JavaScript​

Definition​

  • Event delegation is a technique where, instead of attaching event listeners to multiple child elements, you attach a single event listener to a parent element and use event.target to determine which child triggered the event.
  • This approach:
    • Uses less memory.
    • Scales better when working with many elements.
    • Works for elements added later without attaching new event listeners.

Example 1​

  • Adding event listeners to every item does not scale.
// HTML:
// <ul id="menu">
// <li class="todo-item"><button>Click</button></li>
// </ul>

document.querySelectorAll('.li').forEach(item => {
item.addEventListener('click', handleClick)
})

// Items added later won't have event listeners.

The Solution​

Attach a single event listener to the parent element.

document.querySelector('.menu').addEventListener('click', (event) => {
// event.target.tagName -> The element that was clicked.
// event.currentTarget.tagName -> The element where the listener is attached.

// matches() only works if the clicked element itself matches '.todo-item'.
// It won't match if, for example, a button inside the <li> is clicked.
if (event.target.matches('.todo-item')) {
handleClick(event)
}
})

Example 2: Element.closest()​

Definition: closest()​

  • The closest() method traverses up the DOM tree to find the nearest ancestor (or the element itself) that matches a selector.
// HTML:
//
// <ul id="todo-list">
// <li data-id="1">Buy groceries</li>
// <li data-id="2">Walk the dog</li>
// <li data-id="3">Finish report</li>
// </ul>

const todoList = document.getElementById('todo-list')

todoList.addEventListener('click', (event) => {
// Works regardless of where inside the <li> the user clicks.
const item = event.target.closest('li')

if (item) {
const id = item.dataset.id

console.log(`Clicked todo item with id: ${id}`)

item.classList.toggle('completed')
}
})

Custom Events​

What Are Custom Events?​

  • Custom events let you create your own event types, attach any data you want, and build applications where components communicate through events instead of direct function calls.
  • Use cases:
    1. Notify the application when a modal opens or closes. Listeners then can
      • Pause background videos.
      • Disable page scrolling.
    2. Shopping Cart Updates, Instead of updating multiple UI elements manually. Listeners then can
      • Update the cart badge.
      • Recalculate totals.
    3. A notification component listens and displays the message.
    4. Components can update app theme themselves without knowing who initiated the change.

Example 1​

const event = new CustomEvent('userLoggedIn', {
detail: {
username: 'alice',
timestamp: Date.now()
}
})

// Listen for the event anywhere in your app
document.addEventListener('userLoggedIn', (e) => {
console.log(`Welcome, ${e.detail.username}!`)
})

// Dispatch the event
document.dispatchEvent(event)

// Output:
// Welcome, alice!

Observer APIs​

Intersection Observer​

Definition​

  • The Intersection Observer API lets you detect when an element enters, exits, or crosses a specified visibility threshold within a viewport or container element.
  • Performance
    • Runs off the main thread.
    • Optimized by the browser, making it much more efficient.
  • Use Cases
    • Lazy-load images only when they intersect with the viewport.
    • Build infinite scrolling by loading new items when the bottom is/about reached.
    • Trigger scroll-based animations when elements enter the viewport.

Example​

// Lazy-load images when they come into view
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target
img.src = img.dataset.src

// Never forget to disconnect observers, which can lead to memory leaks.
observer.unobserve(img)
}
})
})

document
.querySelectorAll('img[data-src]')
.forEach(img => observer.observe(img))

MutationObserver​

Definition​

  • MutationObserver watches for DOM changes. It fires a callback when elements are added or removed, attributes change, or text content changes.
  • MutationObserver is asynchronous and batches changes.
  • Use Cases
    • Automatically highlight code blocks added to the page.
    • Block ads or unwanted elements injected by third-party scripts.
    • Detect when form content changes and trigger auto-save.

Example 1​

// Watch for any changes to a DOM element
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
if (mutation.type === 'childList') {
console.log('Children changed!')
console.log('Added:', mutation.addedNodes)
console.log('Removed:', mutation.removedNodes)
}

if (mutation.type === 'attributes') {
console.log(`Attribute "${mutation.attributeName}" changed`)
}
}
})

const targetElement = document.getElementById('app')

observer.observe(targetElement, {
childList: true, // Watch for added/removed children
attributes: true, // Watch for attribute changes
characterData: true // Watch for text content changes
})

Example 2: Blocking Injected Elements​

const adBlocker = new MutationObserver((mutations) => {
for (const mutation of mutations) {
for (const node of mutation.addedNodes) {
if (node.nodeType !== Node.ELEMENT_NODE) continue

if (node.matches('.ad-banner, [data-ad], .sponsored')) {
node.remove()
console.log('Blocked unwanted element')
}
}
}
})

adBlocker.observe(document.body, {
childList: true,
subtree: true
})

ResizeObserver​

Definition​

  • The ResizeObserver API lets you watch elements for size changes and react accordingly.
  • Use Cases
    • Adjust font size based on container width without media queries.
    • Keep a canvas sharp at any size by matching its internal resolution.
    • Keep a chat window scrolled to the bottom when new messages arrive.
    • Maintain the aspect ratio of responsive video or image containers.

Example​

const observer = new ResizeObserver((entries) => {
for (const entry of entries) {
console.log('Element resized:', entry.target)
console.log('New width:', entry.contentRect.width)
console.log('New height:', entry.contentRect.height)
}
})

observer.observe(document.querySelector('.resizable-box'))

PerformanceObserver​

Definition​

  • The Performance Observer API lets you monitor performance metrics in real time. Instead of polling for data, you subscribe to specific performance events and get notified when they occur.
  • Used By
    • Tools like Google Analytics to measure Core Web Vitals.

Some Entry Types​

Entry TypeDescriptionUse
navigationPage navigation timingMeasure page load performance
resourceNetwork requests for scripts, styles, images, etc.Track asset loading times
paintFirst Paint (FP) and First Contentful Paint (FCP)Track rendering milestones
largest-contentful-paintLCP metric (Core Web Vital)Measure loading performance

The buffered: true Option​

  • The buffered option captures performance entries that occurred before the observer started listening.

Example​

// Create an observer with a callback function
const observer = new PerformanceObserver((list, observer) => {
// Called whenever new performance entries are recorded
const entries = list.getEntries()

entries.forEach((entry) => {
console.log(`Entry type: ${entry.entryType}`)
console.log(`Name: ${entry.name}`)
console.log(`Start time: ${entry.startTime}`)
console.log(`Duration: ${entry.duration}`)
})
})

// Start observing specific entry types
observer.observe({
entryTypes: ['resource', 'navigation'],
buffered: true
})

Blob and File API​

Blob​

Definition​

  • A Blob (Binary Large Object) is an immutable, file-like object that represents raw binary data.

Example​

// Creating Blobs from different data types
const textBlob = new Blob(['Hello, World!'], {
type: 'text/plain'
})

const jsonBlob = new Blob(
[JSON.stringify({ name: 'Alice' })],
{ type: 'application/json' }
)

const htmlBlob = new Blob(['<h1>Title</h1>'], {
type: 'text/html'
})

console.log(textBlob.size) // 13 (bytes)
console.log(textBlob.type) // "text/plain"

  • You can create a url from a blob
    • You need to clean the object with revokeObjectURL
const url = URL.createObjectURL(jsonBlob)

const link = document.createElement('a')
link.href = url
link.download = 'hello.txt'
link.click()

URL.revokeObjectURL(url) // Clean up memory

File API​

Getting Files from User Input​

// HTML:
// <input type="file" id="fileInput" multiple>

const fileInput = document.getElementById('fileInput')

fileInput.addEventListener('change', (event) => {
const files = event.target.files // FileList object

for (const file of files) {
console.log('Name:', file.name) // "photo.jpg"
console.log('Size:', file.size) // 1024000 (bytes)
console.log('Type:', file.type) // "image/jpeg"
console.log('Modified:', file.lastModified) // 1704067200000 (timestamp)
console.log('Modified Date:', new Date(file.lastModified))
}
})

Creating File Objects Programmatically​

  • A File is a Blob that represents an actual file.
    • A File inherits from Blob and adds file-specific metadata.
    • Addition metadata File adds include
      • file.name
      • file.lastModified
// Syntax:
// new File(fileBits, fileName, options)

const file = new File(
['Hello, World!'], // Content (same as Blob)
'greeting.txt', // File name
{
type: 'text/plain', // MIME type
lastModified: Date.now() // Optional timestamp
}
)

console.log(file.name) // "greeting.txt"
console.log(file.size) // 13
console.log(file.type) // "text/plain"

Uploading A File in Chunks​

async function uploadInChunks(file, chunkSize = 1024 * 1024) { // 1 MB chunks
const totalChunks = Math.ceil(file.size / chunkSize)

for (let i = 0; i < totalChunks; i++) {
const start = i * chunkSize
const end = Math.min(start + chunkSize, file.size)
const chunk = file.slice(start, end)

const formData = new FormData()
formData.append('chunk', chunk)
formData.append('chunkIndex', i)
formData.append('totalChunks', totalChunks)
formData.append('filename', file.name)

await fetch('/api/upload-chunk', {
method: 'POST',
body: formData
})

console.log(`Uploaded chunk ${i + 1}/${totalChunks}`)
}
}

Web Worker​

  • Mozilla Web Workers documentation
  • Web Workers are a simple means for web content to run scripts in background threads. The worker thread can perform tasks without interfering with the user interface.
    • In simple terms, web Workers give you parallelism, while async/await and Promises give you concurrency
  • What you can and can't inside web workers
    • You can run whatever code you like inside the worker thread, with some exceptions.
      • For example, you can't directly manipulate the DOM from inside a worker, or use some default methods and properties of the window object
    • You can use a large number of items available under window, including WebSockets, and data storage mechanisms like IndexedDB.
  • There are different types of Web workers with different purposes
    • Dedicated Workers
      • Background computation, within a single tab
    • Shared Workers
      • Shared computation across tabs
    • Service Workers
      • Network proxy, offline

Create a Web Worker​

const worker = new Worker('worker-file.js');

Send Message & Responses​

  • Both the client and server use postMessage to send messages & responses to each other
    • The client uses it to send request
    • The server uses it to send response
const worker = new Worker('worker.js', { type: 'module' })

worker.postMessage({ task: 'process', data: [1, 2, 3] })

worker.onmessage = (event) => {
console.log('Result:', event.data)
}

worker.onerror = (event) => {
console.error('Worker error:', event.message)
}

Receive Message & Response​

  • Both client and server respond to messages via the onmessage event handler
    • The message is contained within the message event's data attribute.
    • The data is copied rather than shared, which avoid traditional race conditions issues.
      • If you have a large data like image which you don't want to be copied, you can use a more efficient Transfer Ownership option to transfer the objects ownership instead of copy transfer.
// worker.js

// loads script (old way), processData comes from global scope
importScripts('./utils.js')

// Standard ES modules! (modern way)
import { processData } from './utils.js'

self.onmessage = (event) => {
const { task, data } = event.data

if (task === 'process') {
const result = processData(data)
self.postMessage(result)
}
}

Scheduling APIs​

requestAnimationFrame​

Definition​

  • requestAnimationFrame (often abbreviated as rAF) is a browser API that tells the browser you want to perform an animation.
  • It requests a callback to be executed just before the browser performs its next repaint, typically at 60 frames per second (60 FPS) on most displays.
  • Note
    • requestAnimationFrame is one-shot by design. You must call requestAnimationFrame() inside the callback to continue the animation.

Example​

// The browser calls this function when it's ready to paint
function drawFrame(timestamp) {
// timestamp = milliseconds since page load
console.log(`Frame at ${timestamp}ms`)

// Do your animation work here
updatePosition()

// Request the next frame
requestAnimationFrame(drawFrame)
}

// Start the animation loop
requestAnimationFrame(drawFrame)

Example​

const box = document.getElementById('box');

let position = 0;

function animate() {
position += 2; // move 2px per frame

box.style.left = position + 'px';

// stop condition
if (position < 500) {
requestAnimationFrame(animate);
}
}

// start animation
requestAnimationFrame(animate);

requestIdleCallback​

  • requestIdleCallback() lets the browser run work when it is idle and not busy handling:
    • User input
    • Rendering
    • Animations
    • Higher-priority tasks

Example:

requestIdleCallback(() => {
expensiveAnalyticsWork();
});

Useful for:

  • Analytics
  • Logging
  • Prefetching data
  • Non-critical computations

Example with remaining idle time:

requestIdleCallback((deadline) => {
while (deadline.timeRemaining() > 0) {
processNextItem();
}
});

Example with timeout:

requestIdleCallback(
() => {
saveAnalytics();
},
{ timeout: 2000 }
);

This guarantees the callback runs within ~2 seconds even if the browser never becomes idle.

Caveats​

  • Not supported in all browsers.
  • Never use for critical UI updates.
  • Browser decides when "idle" time exists.

For animation work, prefer:

requestAnimationFrame()

For background/non-urgent work, prefer:

requestIdleCallback()